/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Originally copied from org.eclipse.jdt.internal.corext.refactoring.reorg.MoveCuUpdateCreator
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.ls.core.internal.corext.refactoring.reorg;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.TypeReferenceMatch;
import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.refactoring.SearchResultGroup;
import org.eclipse.jdt.internal.corext.refactoring.changes.TextChangeCompatibility;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.Messages;
import org.eclipse.jdt.ls.core.internal.corext.refactoring.CollectingSearchRequestor;
import org.eclipse.jdt.ls.core.internal.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.jdt.ls.core.internal.corext.refactoring.RefactoringScopeFactory;
import org.eclipse.jdt.ls.core.internal.corext.refactoring.RefactoringSearchEngine;
import org.eclipse.jdt.ls.core.internal.corext.refactoring.base.ReferencesInBinaryContext;
import org.eclipse.jdt.ls.core.internal.corext.refactoring.structure.ReferenceFinderUtil;
import org.eclipse.jdt.ls.core.internal.corext.util.SearchUtils;
import org.eclipse.jdt.ls.core.internal.corext.util.TextChangeManager;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;

public class MoveCuUpdateCreator {

	private final String fNewPackage;
	private ICompilationUnit[] fCus;
	private IPackageFragment fDestination;

	private Map<ICompilationUnit, ImportRewrite> fImportRewrites; //ICompilationUnit -> ImportEdit

	public MoveCuUpdateCreator(ICompilationUnit cu, IPackageFragment pack) {
		this(new ICompilationUnit[] { cu }, pack);
	}

	public MoveCuUpdateCreator(ICompilationUnit[] cus, IPackageFragment pack) {
		Assert.isNotNull(cus);
		Assert.isNotNull(pack);
		fCus = cus;
		fDestination = pack;
		fImportRewrites = new HashMap<>();
		fNewPackage = fDestination.isDefaultPackage() ? "" : fDestination.getElementName() + '.'; //$NON-NLS-1$
	}

	public TextChangeManager createChangeManager(IProgressMonitor pm, RefactoringStatus status) throws JavaModelException {
		pm.beginTask("", 5); //$NON-NLS-1$
		try {
			TextChangeManager changeManager = new TextChangeManager();
			addUpdates(changeManager, new SubProgressMonitor(pm, 4), status);
			addImportRewriteUpdates(changeManager);
			return changeManager;
		} catch (JavaModelException e) {
			throw e;
		} catch (CoreException e) {
			throw new JavaModelException(e);
		} finally {
			pm.done();
		}

	}

	private void addImportRewriteUpdates(TextChangeManager changeManager) throws CoreException {
		for (Iterator<ICompilationUnit> iter = fImportRewrites.keySet().iterator(); iter.hasNext();) {
			ICompilationUnit cu = iter.next();
			ImportRewrite importRewrite = fImportRewrites.get(cu);
			if (importRewrite != null && importRewrite.hasRecordedChanges()) {
				TextChangeCompatibility.addTextEdit(changeManager.get(cu), RefactoringCoreMessages.MoveCuUpdateCreator_update_imports, importRewrite.rewriteImports(null));
			}
		}
	}

	private void addUpdates(TextChangeManager changeManager, IProgressMonitor pm, RefactoringStatus status) throws CoreException {
		pm.beginTask("", fCus.length); //$NON-NLS-1$
		for (int i = 0; i < fCus.length; i++) {
			if (pm.isCanceled()) {
				throw new OperationCanceledException();
			}

			addUpdates(changeManager, fCus[i], new SubProgressMonitor(pm, 1), status);
		}
	}

	private void addUpdates(TextChangeManager changeManager, ICompilationUnit movedUnit, IProgressMonitor pm, RefactoringStatus status) throws CoreException {
		try {
			pm.beginTask("", 3); //$NON-NLS-1$
			pm.subTask(Messages.format(RefactoringCoreMessages.MoveCuUpdateCreator_searching, BasicElementLabels.getFileName(movedUnit)));

			if (isInAnotherFragmentOfSamePackage(movedUnit, fDestination)) {
				pm.worked(3);
				return;
			}

			addImportToSourcePackageTypes(movedUnit, new SubProgressMonitor(pm, 1));
			removeImportsToDestinationPackageTypes(movedUnit);
			addReferenceUpdates(changeManager, movedUnit, new SubProgressMonitor(pm, 2), status);
		} finally {
			pm.done();
		}
	}

	private void addReferenceUpdates(TextChangeManager changeManager, ICompilationUnit movedUnit, IProgressMonitor pm, RefactoringStatus status) throws JavaModelException, CoreException {
		List<ICompilationUnit> cuList = Arrays.asList(fCus);
		SearchResultGroup[] references = getReferences(movedUnit, pm, status);
		for (int i = 0; i < references.length; i++) {
			SearchResultGroup searchResultGroup = references[i];
			ICompilationUnit referencingCu = searchResultGroup.getCompilationUnit();
			if (referencingCu == null) {
				continue;
			}

			boolean simpleReferencesNeedNewImport = simpleReferencesNeedNewImport(movedUnit, referencingCu, cuList);
			SearchMatch[] results = searchResultGroup.getSearchResults();
			for (int j = 0; j < results.length; j++) {
				// TODO: should update type references with results from addImport
				TypeReference reference = (TypeReference) results[j];
				if (reference.isImportDeclaration()) {
					ImportRewrite rewrite = getImportRewrite(referencingCu);
					IImportDeclaration importDecl = (IImportDeclaration) SearchUtils.getEnclosingJavaElement(results[j]);
					if (Flags.isStatic(importDecl.getFlags())) {
						rewrite.removeStaticImport(importDecl.getElementName());
						addStaticImport(movedUnit, importDecl, rewrite);
					} else {
						rewrite.removeImport(importDecl.getElementName());
						rewrite.addImport(createStringForNewImport(movedUnit, importDecl));
					}
				} else if (reference.isQualified()) {
					TextChange textChange = changeManager.get(referencingCu);
					String changeName = RefactoringCoreMessages.MoveCuUpdateCreator_update_references;
					TextEdit replaceEdit = new ReplaceEdit(reference.getOffset(), reference.getSimpleNameStart() - reference.getOffset(), fNewPackage);
					TextChangeCompatibility.addTextEdit(textChange, changeName, replaceEdit);
				} else if (simpleReferencesNeedNewImport) {
					ImportRewrite importEdit = getImportRewrite(referencingCu);
					String typeName = reference.getSimpleName();
					importEdit.addImport(getQualifiedType(fDestination.getElementName(), typeName));
				}
			}
		}
	}

	private void addStaticImport(ICompilationUnit movedUnit, IImportDeclaration importDecl, ImportRewrite rewrite) {
		String old = importDecl.getElementName();
		int oldPackLength = movedUnit.getParent().getElementName().length();

		StringBuilder result = new StringBuilder(fDestination.getElementName());
		if (oldPackLength == 0) {
			result.append('.').append(old);
		} else if (result.length() == 0) {
			result.append(old.substring(oldPackLength + 1)); // cut "."
		} else {
			result.append(old.substring(oldPackLength));
		}
		int index = result.lastIndexOf("."); //$NON-NLS-1$
		if (index > 0 && index < result.length() - 1) {
			rewrite.addStaticImport(result.substring(0, index), result.substring(index + 1, result.length()), true);
		}
	}

	private String getQualifiedType(String packageName, String typeName) {
		if (packageName.length() == 0) {
			return typeName;
		} else {
			return packageName + '.' + typeName;
		}
	}

	private String createStringForNewImport(ICompilationUnit movedUnit, IImportDeclaration importDecl) {
		String old = importDecl.getElementName();
		int oldPackLength = movedUnit.getParent().getElementName().length();

		StringBuilder result = new StringBuilder(fDestination.getElementName());
		if (oldPackLength == 0) {
			result.append('.').append(old);
		} else if (result.length() == 0) {
			result.append(old.substring(oldPackLength + 1)); // cut "."
		} else {
			result.append(old.substring(oldPackLength));
		}
		return result.toString();
	}

	private void removeImportsToDestinationPackageTypes(ICompilationUnit movedUnit) throws CoreException {
		ImportRewrite importEdit = getImportRewrite(movedUnit);
		IType[] destinationTypes = getDestinationPackageTypes();
		for (int i = 0; i < destinationTypes.length; i++) {
			importEdit.removeImport(destinationTypes[i].getFullyQualifiedName('.'));
		}
	}

	private IType[] getDestinationPackageTypes() throws JavaModelException {
		List<IType> types = new ArrayList<>();
		if (fDestination.exists()) {
			ICompilationUnit[] cus = fDestination.getCompilationUnits();
			for (int i = 0; i < cus.length; i++) {
				types.addAll(Arrays.asList(cus[i].getTypes()));
			}
		}
		return types.toArray(new IType[types.size()]);
	}

	private void addImportToSourcePackageTypes(ICompilationUnit movedUnit, IProgressMonitor pm) throws CoreException {
		List<ICompilationUnit> cuList = Arrays.asList(fCus);
		IType[] allCuTypes = movedUnit.getAllTypes();
		IType[] referencedTypes = ReferenceFinderUtil.getTypesReferencedIn(allCuTypes, pm);
		ImportRewrite importEdit = getImportRewrite(movedUnit);
		importEdit.setFilterImplicitImports(false);
		IPackageFragment srcPack = (IPackageFragment) movedUnit.getParent();
		for (int i = 0; i < referencedTypes.length; i++) {
			IType iType = referencedTypes[i];
			if (!iType.exists()) {
				continue;
			}
			if (!JavaModelUtil.isSamePackage(iType.getPackageFragment(), srcPack)) {
				continue;
			}
			if (cuList.contains(iType.getCompilationUnit())) {
				continue;
			}
			importEdit.addImport(iType.getFullyQualifiedName('.'));
		}
	}

	private ImportRewrite getImportRewrite(ICompilationUnit cu) throws CoreException {
		if (fImportRewrites.containsKey(cu)) {
			return fImportRewrites.get(cu);
		}
		ImportRewrite importEdit = StubUtility.createImportRewrite(cu, true);
		fImportRewrites.put(cu, importEdit);
		return importEdit;
	}

	private boolean simpleReferencesNeedNewImport(ICompilationUnit movedUnit, ICompilationUnit referencingCu, List<ICompilationUnit> cuList) {
		if (referencingCu.equals(movedUnit)) {
			return false;
		}
		if (cuList.contains(referencingCu)) {
			return false;
		}
		if (isReferenceInAnotherFragmentOfSamePackage(referencingCu, movedUnit)) {
			/* Destination package is different from source, since
			 * isDestinationAnotherFragmentOfSamePackage(movedUnit) was false in addUpdates(.) */
			return true;
		}

		//heuristic
		if (referencingCu.getImport(movedUnit.getParent().getElementName() + ".*").exists()) {
			return true; // has old star import
		}
		if (referencingCu.getParent().equals(movedUnit.getParent())) {
			return true; //is moved away from same package
		}
		return false;
	}

	private boolean isReferenceInAnotherFragmentOfSamePackage(ICompilationUnit referencingCu, ICompilationUnit movedUnit) {
		if (referencingCu == null) {
			return false;
		}
		if (!(referencingCu.getParent() instanceof IPackageFragment)) {
			return false;
		}
		IPackageFragment pack = (IPackageFragment) referencingCu.getParent();
		return isInAnotherFragmentOfSamePackage(movedUnit, pack);
	}

	private static boolean isInAnotherFragmentOfSamePackage(ICompilationUnit cu, IPackageFragment pack) {
		if (!(cu.getParent() instanceof IPackageFragment)) {
			return false;
		}
		IPackageFragment cuPack = (IPackageFragment) cu.getParent();
		return !cuPack.equals(pack) && JavaModelUtil.isSamePackage(cuPack, pack);
	}

	private static SearchResultGroup[] getReferences(ICompilationUnit unit, IProgressMonitor pm, RefactoringStatus status) throws CoreException {
		final SearchPattern pattern = RefactoringSearchEngine.createOrPattern(unit.getTypes(), IJavaSearchConstants.REFERENCES);
		if (pattern != null) {
			String binaryRefsDescription = Messages.format(RefactoringCoreMessages.ReferencesInBinaryContext_ref_in_binaries_description, BasicElementLabels.getFileName(unit));
			ReferencesInBinaryContext binaryRefs = new ReferencesInBinaryContext(binaryRefsDescription);
			Collector requestor = new Collector(((IPackageFragment) unit.getParent()), binaryRefs);
			IJavaSearchScope scope = RefactoringScopeFactory.create(unit, true, false);

			SearchResultGroup[] result = RefactoringSearchEngine.search(pattern, scope, requestor, new SubProgressMonitor(pm, 1), status);
			binaryRefs.addErrorIfNecessary(status);
			return result;
		}
		return new SearchResultGroup[] {};
	}

	private final static class Collector extends CollectingSearchRequestor {
		private IPackageFragment fSource;
		private IScanner fScanner;

		public Collector(IPackageFragment source, ReferencesInBinaryContext binaryRefs) {
			super(binaryRefs);
			fSource = source;
			fScanner = ToolFactory.createScanner(false, false, false, false);
		}

		@Override
		public void acceptSearchMatch(SearchMatch match) throws CoreException {
			if (filterMatch(match)) {
				return;
			}

			/*
			 * Processing is done in collector to reuse the buffer which was
			 * already required by the search engine to locate the matches.
			 */
			// [start, end[ include qualification.
			IJavaElement element = SearchUtils.getEnclosingJavaElement(match);
			int accuracy = match.getAccuracy();
			int start = match.getOffset();
			int length = match.getLength();
			boolean insideDocComment = match.isInsideDocComment();
			IResource res = match.getResource();
			if (element.getAncestor(IJavaElement.IMPORT_DECLARATION) != null) {
				collectMatch(TypeReference.createImportReference(element, accuracy, start, length, insideDocComment, res));
			} else {
				ICompilationUnit unit = (ICompilationUnit) element.getAncestor(IJavaElement.COMPILATION_UNIT);
				if (unit != null) {
					IBuffer buffer = unit.getBuffer();
					String matchText = buffer.getText(start, length);
					if (fSource.isDefaultPackage()) {
						collectMatch(TypeReference.createSimpleReference(element, accuracy, start, length, insideDocComment, res, matchText));
					} else {
						// assert: matchText doesn't start nor end with comment
						int simpleNameStart = getLastSimpleNameStart(matchText);
						if (simpleNameStart != 0) {
							collectMatch(TypeReference.createQualifiedReference(element, accuracy, start, length, insideDocComment, res, start + simpleNameStart));
						} else {
							collectMatch(TypeReference.createSimpleReference(element, accuracy, start, length, insideDocComment, res, matchText));
						}
					}
				}
			}
		}

		private int getLastSimpleNameStart(String reference) {
			fScanner.setSource(reference.toCharArray());
			int lastIdentifierStart = -1;
			try {
				int tokenType = fScanner.getNextToken();
				while (tokenType != ITerminalSymbols.TokenNameEOF) {
					if (tokenType == ITerminalSymbols.TokenNameIdentifier) {
						lastIdentifierStart = fScanner.getCurrentTokenStartPosition();
					}
					tokenType = fScanner.getNextToken();
				}
			} catch (InvalidInputException e) {
				JavaLanguageServerPlugin.logException(e.getMessage(), e);
			}
			return lastIdentifierStart;
		}
	}

	private final static class TypeReference extends TypeReferenceMatch {
		private String fSimpleTypeName;
		private int fSimpleNameStart;

		private TypeReference(IJavaElement enclosingElement, int accuracy, int start, int length, boolean insideDocComment, IResource resource, int simpleNameStart, String simpleName) {
			super(enclosingElement, accuracy, start, length, insideDocComment, SearchEngine.getDefaultSearchParticipant(), resource);
			fSimpleNameStart = simpleNameStart;
			fSimpleTypeName = simpleName;
		}

		public static TypeReference createQualifiedReference(IJavaElement enclosingElement, int accuracy, int start, int length, boolean insideDocComment, IResource resource, int simpleNameStart) {
			Assert.isTrue(start < simpleNameStart && simpleNameStart < start + length);
			return new TypeReference(enclosingElement, accuracy, start, length, insideDocComment, resource, simpleNameStart, null);
		}

		public static TypeReference createImportReference(IJavaElement enclosingElement, int accuracy, int start, int length, boolean insideDocComment, IResource resource) {
			return new TypeReference(enclosingElement, accuracy, start, length, insideDocComment, resource, -1, null);
		}

		public static TypeReference createSimpleReference(IJavaElement enclosingElement, int accuracy, int start, int length, boolean insideDocComment, IResource resource, String simpleName) {
			return new TypeReference(enclosingElement, accuracy, start, length, insideDocComment, resource, -1, simpleName);
		}

		public boolean isImportDeclaration() {
			return SearchUtils.getEnclosingJavaElement(this).getAncestor(IJavaElement.IMPORT_DECLARATION) != null;
		}

		public boolean isQualified() {
			return fSimpleNameStart != -1;
		}

		/**
		 * @return start offset of simple type name, or -1 iff ! isQualified()
		 */
		public int getSimpleNameStart() {
			return fSimpleNameStart;
		}

		/**
		 * @return simple type name, or null iff ! isSimpleName()
		 */
		public String getSimpleName() {
			return fSimpleTypeName;
		}
	}

}
